﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Argos.Collections;
using System.IO;

namespace Argos.Scheduling
{
	/// <summary>
	/// Represents a weekly schedule.
	/// </summary>
	/// <remarks>
	/// A weekly schedule where occurrences occur regularly from week to week.
	/// </remarks>
	[Serializable]
	[DisplayName("Weekly Schedule")]
	[Description("A weekly schedule where occurrences occur regularly from week to week.")]
	public sealed class WeeklySchedule : Schedule
	{

		#region public static WeeklySchedule FromOccurrences(params Occurrence[] occurrences)

		/// <summary>
		/// Creates a weekly schedule from the given occurrences. The weekdays and time stamps
		/// from the given occurrences are used, but the actual dates are ignored.
		/// </summary>
		/// <param name="occurrences">The occurrences to create the weekly schedule from.</param>
		public static WeeklySchedule FromOccurrences(params Occurrence[] occurrences)
		{
			WeeklySchedule schedule = new WeeklySchedule();

			if (null != occurrences)
			{
				foreach (Occurrence dt in occurrences)
				{
					schedule.Entries.Add(new Entry(dt.Start.DayOfWeek, new TimeOfDay(dt.Start), dt.Duration));
				}
			}

			return schedule;
		}

		#endregion

		#region public static new WeeklySchedule Read(Stream stream)

		/// <summary>
		/// Reads the weekly schedule from the given stream.
		/// </summary>
		/// <param name="stream">The stream to read the schedule from.</param>
		public static new WeeklySchedule Read(Stream stream)
		{
			return Schedule.Read<WeeklySchedule>(stream);
		}

		#endregion

		#region public static new WeeklySchedule Read(byte[] buffer)

		/// <summary>
		/// Reads the schedule from the given buffer.
		/// </summary>
		/// <param name="buffer">The buffer to read the schedule from.</param>
		public static new WeeklySchedule Read(byte[] buffer)
		{
			return Schedule.Read<WeeklySchedule>(buffer);
		}

		#endregion



		#region public void AddAllDays(TimeOfDay start)

		/// <summary>
		/// Adds a new schedule entry for all weekdays with a time slot with the given start time and a zero duration.
		/// </summary>
		/// <param name="start">The start time for each new schedule entry.</param>
		public void AddAllDays(TimeOfDay start)
		{
			this.AddAllDays(start, TimeSpan.Zero);
		}

		#endregion
		
		#region public void AddAllDays(TimeOfDay start, TimeSpan duration)

		/// <summary>
		/// Adds a new schedule entry for all weekdays with a time slot with the given start and duration.
		/// </summary>
		/// <param name="start">The start time of the time slot of each new schedule entry.</param>
		/// <param name="duration">The duration of the time slot of each new schedule entry.</param>
		public void AddAllDays(TimeOfDay start, TimeSpan duration)
		{
			Predicate<DayOfWeek> matcher = delegate(DayOfWeek day)
			{
				return true;
			};
			this.AddSelectedDays(matcher, start, duration);
		}

		#endregion

		#region public void AddSelectedDays(Predicate<DayOfWeek> dayMatcher, TimeOfDay start)

		/// <summary>
		/// Adds a new schedule entry for each day of the week that are matched by the given
		/// day matcher using a time slot with the given start time.
		/// </summary>
		/// <param name="dayMatcher">The predicate that matches each weekday to add as a new schedule entry.</param>
		/// <param name="start">The start time for each new schedule entry.</param>
		public void AddSelectedDays(Predicate<DayOfWeek> dayMatcher, TimeOfDay start)
		{
			this.AddSelectedDays(dayMatcher, start, TimeSpan.Zero);
		}

		#endregion

		#region public void AddSelectedDays(Predicate<DayOfWeek> dayMatcher, TimeOfDay start, TimeSpan duration)

		/// <summary>
		/// Adds a new schedule entry for each day of the week that are matched by the given
		/// day matcher using a time slot with the given start time and duration.
		/// </summary>
		/// <param name="dayMatcher">The predicate that matches each weekday to add as a new schedule entry.</param>
		/// <param name="start">The start time for each new schedule entry.</param>
		/// <param name="duration">The duration for each new time slot.</param>
		public void AddSelectedDays(Predicate<DayOfWeek> dayMatcher, TimeOfDay start, TimeSpan duration)
		{
			WeekdayList list = WeekdayList.AllDays;

			foreach (DayOfWeek day in list)
			{
				if (dayMatcher.Invoke(day))
				{
					this.Entries.Add(new Entry(day, start, duration));
				}
			}
		}

		#endregion
		
		#region public override void AddDateTime(DateTime dt, TimeSpan duration)

		/// <summary>
		/// Adds a new weekly schedule entry from the given date, time and duration.
		/// </summary>
		/// <param name="dt">The date and time to use with the new entry.</param>
		/// <param name="duration">The duration for the schedule entry.</param>
		public override void AddDateTime(DateTime dt, TimeSpan duration)
		{
			this.Entries.Add(new Entry(dt.DayOfWeek, new TimeOfDay(dt), duration));
		}

		#endregion



		/// <summary>
		/// Represents an entry in a weekly schedule.
		/// </summary>
		[Serializable]
		public sealed class Entry : ScheduleEntry
		{

			#region #ctor: public Entry(DayOfWeek day, TimeOfDay start)

			/// <summary>
			/// Creates a new instance of the class.
			/// </summary>
			/// <param name="day">The weekday for the schedule entry.</param>
			/// <param name="start">The start time for the schedule entry.</param>
			public Entry(DayOfWeek day, TimeOfDay start) : this(day, start, TimeSpan.Zero) { }

			#endregion

			#region #ctor: public Entry(DayOfWeek day, TimeOfDay start, TimeSpan duration)

			/// <summary>
			/// Creates a new instance of the class.
			/// </summary>
			/// <param name="day">The weekday for the schedule entry.</param>
			/// <param name="start">The start time for the schedule entry.</param>
			/// <param name="duration">The duration for the schedule entry.</param>
			public Entry(DayOfWeek day, TimeOfDay start, TimeSpan duration)
			{
				this.Day = day;
				this.Start = start;
				this.Duration = duration;
			}

			#endregion



			#region public DayOfWeek Day { get; private set; }

			/// <summary>
			/// Returns the weekday that the schedule entry specifies.
			/// </summary>
			public DayOfWeek Day { get; private set; }

			#endregion

			#region public TimeSpan Duration { get; private set; }

			/// <summary>
			/// Returns the duration that the schedule entry specifies.
			/// </summary>
			public TimeSpan Duration { get; private set; }

			#endregion

			#region public TimeOfDay Start { get; private set; }

			/// <summary>
			/// Returns the start time that the schedule entry specifies.
			/// </summary>
			public TimeOfDay Start { get; private set; }

			#endregion



			#region public override Occurrence GetNextOccurrence(DateTime after)

			/// <summary>
			/// Returns the next occurrence after the given date and time that is defined by this entry.
			/// </summary>
			/// <param name="after">The date and time after which to return the first starting occurrence.</param>
			/// <returns>Returns the next starting occurrence. Returns null if the entry does not define a next occurrence.</returns>
			public override Occurrence GetNextOccurrence(DateTime after)
			{
				DateTime dt = this.Start.ToDateTime(after.Year, after.Month, after.Day);
				if(dt < after)
				{
					dt = dt.AddDays(1);
				}

				while (dt.DayOfWeek != this.Day)
				{
					dt = dt.AddDays(1);
				}

				return new Occurrence(dt, this.Duration);
			}

			#endregion

			#region public override Occurrence GetPreviousOccurrence(DateTime before)

			/// <summary>
			/// Returns the previous occurrence before the given date and time that is defined by this entry.
			/// </summary>
			/// <param name="before">The date and time before which to return the last starting occurrence.</param>
			/// <returns>Returns the last starting occurrence. Returns null if the entry does not define a previous occurrence.</returns>
			public override Occurrence GetPreviousOccurrence(DateTime before)
			{
				DateTime dt = this.Start.ToDateTime(before.Year, before.Month, before.Day);
				if (dt > before)
				{
					dt = dt.AddDays(-1);
				}

				while (dt.DayOfWeek != this.Day)
				{
					dt = dt.AddDays(-1);
				}

				return new Occurrence(dt, this.Duration);
			}

			#endregion
			
			#region public override string ToString()

			/// <summary>
			/// Returns the string representation of the schedule entry.
			/// </summary>
			public override string ToString()
			{
				if (this.Duration != TimeSpan.Zero)
				{
					return string.Format("{0} at {1} to {2}", this.Day, this.Start, this.Start.Add(this.Duration));
				}
				else
				{
					return string.Format("{0} at {1}", this.Day, this.Start);
				}
			}

			#endregion

		}

	}
}
